/*
 *      Copyright 2012-2013 Battams, Derek
 *       
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 */
package org.schedulesdirect.api;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.schedulesdirect.api.exception.InvalidJsonObjectException;

/**
 * A Lineup represents a single television lineup available in a Headend
 * 
 * <p>Only an EpgClient can instantiate objects of this class.</p>
 * 
 * <p>When there's a discrepancy between the descriptions of data found in this source code and
 * that found <a href="https://github.com/rkulagowski/tv_grab_na_sd/wiki">here</a> then the 
 * latter web source will always take precedence!</p>
 * 
 * @author Derek Battams &lt;derek@battams.ca&gt;
 *
 */
public class Lineup {
	
	private String headendId;
	private String name;
	private String device;
	private Date lastModified;
	private String location;
	private Map<String, List<String>> stationMap;
	private Map<String, List<String>> physicalStationMap;
	private Map<String, Station> stations;
	private EpgClient epgClnt;
	private boolean physicalMapping;
	
	/**
	 * Constructor
	 * @param headendId The headend id this lineup belongs to
	 * @param name The name of the lineup; e.g. Shaw Direct Satellite
	 * @param location The location of the lineup; e.g. Madison, WI
	 * @param metadata A JSON object representing the metadata associated with this Lineup; obtained from the raw upstream feed
	 * @param stations A JSON array of JSON objects representing all stations available in this lineup; obtained from the raw upstream feed
	 * @param stationMap A JSON array of JSON objects representing the channel mappings for this lineup; usually only provided for cable/satellite lineups; obtained from the raw upstream feed
	 * @param clnt The EpgClient instance this object is to be attached to
	 * @throws InvalidJsonObjectException Thrown if any of the JSON arguments are not in the expected format
	 */
	Lineup(String headendId, String name, String location, JSONObject metadata, JSONArray stations, JSONArray stationMap, EpgClient clnt) throws InvalidJsonObjectException {
		epgClnt = clnt;
		this.headendId = headendId;
		this.stationMap = new HashMap<String, List<String>>();
		this.physicalStationMap = new HashMap<String, List<String>>();
		this.stations = new HashMap<String, Station>();
		Map<Integer, JSONObject> tuningDetails = new HashMap<Integer, JSONObject>();
		try {
			for(int i = 0; i < stationMap.length(); ++i) {
				JSONObject o = stationMap.getJSONObject(i);
				tuningDetails.put(o.getInt("stationID"), o);
				if(o.has("uhfVhf"))
					physicalMapping = true;
			}
			device = metadata.getString("device");
			lastModified = Config.get().getDateTimeFormat().parse(metadata.getString("modified"));
			this.location = location;
			this.name = name;
			for(int i = 0; i < stations.length(); ++ i) {
				JSONObject sObj = stations.getJSONObject(i);
				JSONObject tuningInfo = tuningDetails.get(sObj.getInt("stationID"));
				if(tuningInfo != null) {
					Station s = new Station(sObj, tuningInfo, epgClnt);
					this.stations.put(s.getId(), s);
				}
			}
			if(physicalMapping)
				buildChannelMapViaAtscData();
			else
				buildChannelMapViaJson(stationMap);
		} catch (Exception e) {
			throw new InvalidJsonObjectException(e);
		}
	}
	
	/**
	 * Build the channel map member via JSON input
	 * @param src The JSON array containing the channel mappings for this lineup; received from raw upstream feed
	 * @throws JSONException Thrown if the argument is not in the expected format
	 */
	protected void buildChannelMapViaJson(JSONArray src) throws JSONException {
		for(int i = 0; i < src.length(); ++i) {
			JSONObject o = src.getJSONObject(i);
			List<String> list = stationMap.get(o.getString("stationID"));
			if(list == null) {
				list = new ArrayList<String>();
				stationMap.put(o.getString("stationID"), list);
			}
			list.add(o.getString("channel").replaceAll("\\.", "-"));
		}
	}

	/**
	 * 
	 * @return The headend id this lineup belongs to
	 */
	public String getHeadendId() { return headendId; }
	
	/**
	 * Build the channel map from OTA info
	 * 
	 * <p>The map is built from the OTA info of each station object contained in this instance</p>
	 */
	protected void buildChannelMapViaAtscData() {
		boolean diffLogicalVsPhysical = false;
		for(Station s : stations.values()) {
			String pNum = s.getPhysicalChannelNumber();
			String lNum = s.getLogicalChannelNumber();
			if(lNum == null)
				lNum = pNum;
			if(lNum != null) {
				List<String> list = stationMap.get(s.getId());
				if(list == null) {
					list = new ArrayList<String>();
					stationMap.put(s.getId(), list);
				}
				list.add(lNum);
			}
			if(pNum != null) {
				List<String> list = physicalStationMap.get(s.getId());
				if(list == null) {
					list = new ArrayList<String>();
					physicalStationMap.put(s.getId(), list);
				}
				list.add(pNum);
			}
			if(pNum != null && lNum != null && !pNum.equals(lNum))
				diffLogicalVsPhysical = true;
		}
		physicalMapping = diffLogicalVsPhysical;
	}

	/**
	 * @return Returns true if this lineup has a physical channel mapping that differs from its logical channel mapping; this will usually occur for most OTA lineups
	 */
	public boolean hasPhysicalMapping() {
		return physicalMapping;
	}
	
	/**
	 * @return The device name for this lineup; the device name is "Antenna" for OTA lineups and "X" for cable/sat, additional letters may appear for temp lineups within a cable/sat headend
	 */
	public String getDevice() {
		return device;
	}

	/**
	 * @return The last time this lineup was modified upstream; use this to cache lineup data
	 */
	public Date getLastModified() {
		return lastModified;
	}

	/**
	 * @return The physical location of this headend (i.e. city/country/etc.); may not always be available
	 */
	public String getLocation() {
		return location;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		final int maxLen = 2;
		StringBuilder builder = new StringBuilder();
		builder.append("Lineup [headendId=");
		builder.append(headendId);
		builder.append(", name=");
		builder.append(name);
		builder.append(", device=");
		builder.append(device);
		builder.append(", lastModified=");
		builder.append(lastModified);
		builder.append(", location=");
		builder.append(location);
		builder.append(", stationMap=");
		builder.append(stationMap != null ? toString(stationMap.entrySet(),
				maxLen) : null);
		builder.append(", physicalStationMap=");
		builder.append(physicalStationMap != null ? toString(
				physicalStationMap.entrySet(), maxLen) : null);
		builder.append(", stations=");
		builder.append(stations != null ? toString(stations.entrySet(), maxLen)
				: null);
		builder.append(", epgClnt=");
		builder.append(epgClnt);
		builder.append(", physicalMapping=");
		builder.append(physicalMapping);
		builder.append("]");
		return builder.toString();
	}

	/**
	 * @return The name of this lineup; may not always be available
	 */
	public String getName() {
		return name;
	}

	/**
	 * @return Returns the mapping of station ids to channel numbers for this lineup; keys are station ids, values are arrays of channel numbers to which the station maps to
	 */
	public Map<String, List<String>> getStationMap() {
		return stationMap;
	}
	
	/**
	 * @return Return the physical station mapping for this lineup; if there is no physical mapping for this lineup then the logical mapping is returned instead
	 */
	public Map<String, List<String>> getPhysicalStationMap() {
		return physicalMapping ? physicalStationMap : stationMap;
	}

	/**
	 * @return The array of stations available on this lineup
	 */
	public Station[] getStations() {
		return stations.values().toArray(new Station[0]);
	}

	/**
	 * Return the Station object for a given Station id in the lineup
	 * @param stationId The station id to fetch
	 * @return The Station object for the given station id or null if it's not available
	 */
	public Station getStation(String stationId) {
		return stations.get(stationId);
	}
	
	// Generated helper for toString()
	private String toString(Collection<?> collection, int maxLen) {
		StringBuilder builder = new StringBuilder();
		builder.append("[");
		int i = 0;
		for (Iterator<?> iterator = collection.iterator(); iterator.hasNext()
				&& i < maxLen; i++) {
			if (i > 0)
				builder.append(", ");
			builder.append(iterator.next());
		}
		builder.append("]");
		return builder.toString();
	}
}
